package it.unipd.dei.es1011.nucularc0d3.defaultproject;

import it.unipd.dei.es1011.nucularc0d3.defaultproject.util.DataWorker;
import it.unipd.dei.es1011.nucularc0d3.defaultproject.util.Details;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;

/**
 * Service used to recording data. We used a service to record data even when the activity is sent to background.
 * 
 * @author Luca Lazzarini
 *
 */
public class RecordingService extends Service { 
	//	Object and variables needed to run the service
	// Keeps track of all current registered clients.
	ArrayList<Messenger> mClients = new ArrayList<Messenger>(); 
	// Target we publish for clients to send messages to IncomingHandler.
	final Messenger mMessenger = new Messenger(new IncomingHandler()); 
	// handler used to retrieve the value from the sensor at specific period
	private Handler myHandler = new Handler();

	//	Notification Manager, used to display information on the top bar
	private NotificationManager notificationManager;
	//	type of message received
	static final int REGISTER_CLIENT = 1;
	static final int UNREGISTER_CLIENT = 2;
	static final int STOP_RECORDING = 3; // stop recording imposed from the client
	//  type of message sent
	static final int MSG_STOP_SERVICE = 0; // self stopping after finish the job
	static final int MSG_SENDING_SAMPLE_COLLECTED = 5;
	static final int MSG_SENDING_ELAPSED_TIME = 6;

	//	Used to retrieve the information from the sensor
	private SensorManager mySensorManager;
	private Sensor mySensor;

	//	temporary value for axis, use atomic value due to concurrent task
	private AtomicReference<Float> xAxisTempValue;
	private AtomicReference<Float> yAxisTempValue;
	private AtomicReference<Float> zAxisTempValue;

	//	list of values that will be passed to allValuesVector
	private LinkedList<Float> xAxisListValue;
	private LinkedList<Float> yAxisListValue;
	private LinkedList<Float> zAxisListValue;

	//	whole vector that will contains all the recorded values
	//	it will be passed to the DataWorker.
	private Vector<Iterator<Float>> allValuesVector;

	//	sample rate and number of sample collected so far, plus other useful var
	private int samplingRate;
	private AtomicInteger numberSampleCollected,seconds,minutes;
	
	//	Time info
	private AtomicLong totalTime,currentTimeClock,startTime;
	
	//	Axis enabled for recording
	private boolean xAxisEnabled,yAxisEnabled,zAxisEnabled;

	//	my instance of Details
	private Details myDetails;

	// due to avoid have more than one worker
	private boolean working = false; 

	//	class used to monitoring the sensor
	private SensorListener mySensorListener;

	/**
	 * Method called when the Activity bind to this service.
	 */
	@Override
	public IBinder onBind(Intent intent) {
		if(!working){
			myHandler.postDelayed(worker,100);
			working = true;
		}
		
		return mMessenger.getBinder();
	}

	/**
	 * Method called when the activity call startService. I retrieve the info from the intent
	 * so i can prepare the class for execution.
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if(startTime.get() == 0){
			startTime.set(System.currentTimeMillis());
		}
		// saving the data passed from the intent to the Details (through the initDetails())
		
		if(!intent.hasExtra("axis")){
			startActivity(new Intent(this, MainActivity.class)); 
			Toast.makeText(getApplicationContext(), 
				    "Lack of parameters, process shut down", 
				    Toast.LENGTH_SHORT).show();
		}
		boolean[] axis = new boolean[3];
		axis = intent.getBooleanArrayExtra("axis");
		xAxisEnabled = axis[0];
		yAxisEnabled = axis[1];
		zAxisEnabled = axis[2];
		
		if(!intent.hasExtra("totalTime")){
			startActivity(new Intent(this, MainActivity.class)); 
			Toast.makeText(getApplicationContext(), 
				    "Lack of parameters, process shut down", 
				    Toast.LENGTH_SHORT).show();
		}
		
		totalTime.set(intent.getLongExtra("totalTime", 1));
		if(!intent.hasExtra("samplingRate")){
			startActivity(new Intent(this, MainActivity.class)); 
			Toast.makeText(getApplicationContext(), 
				    "Lack of parameters, process shut down", 
				    Toast.LENGTH_SHORT).show();
		}
		Resources res = getResources(); // retrieve the value from the XML file /values/arrays.xml
		String[] arraySampleValue = res.getStringArray(R.array.samplingRateValues);
		samplingRate = intent.getIntExtra("samplingRate", new Integer(arraySampleValue[1]));

		initDetails(); // init Detail object (used for saving data)
		return START_STICKY; // run until explicitly stopped.
	}

	/**
	 * This class is used to handle the messages that come from the activity.
	 * @author Luca Lazzarini
	 *
	 */
	class IncomingHandler extends Handler { 
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case REGISTER_CLIENT:
				mClients.add(msg.replyTo);
				break;
			case UNREGISTER_CLIENT:
				mClients.clear();
				break;
			case STOP_RECORDING:
				stopButtonPressed();
				break;
			default:
				super.handleMessage(msg);
			}
		}
	}

	/**
	 * Method called when the user press the stop button. It will update the duration value
	 * in the Detail and signal the service to stop as soon as it can by putting.
	 */
	private void stopButtonPressed(){		
		String minuti = "";
		if(minutes.get() < 10)
			minuti += "0" + minutes.get();
		else
			minuti += "" + minutes.get();

		String secondi = "";
		if(seconds.get() < 10)
			secondi += "0" + seconds.get();
		else
			secondi += "" + seconds.get();

		Log.i("RecordingService","minuti: "+minuti+" secondi: "+secondi);

		myDetails.duration = minuti+":"+secondi;
		totalTime.set(0);
	}

	/**
	 * This class instantiate the basic variables used in the whole service.
	 */
	private void initSystemForRecording(){
		xAxisTempValue = new AtomicReference<Float>(0.0f);
		yAxisTempValue = new AtomicReference<Float>(0.0f);
		zAxisTempValue = new AtomicReference<Float>(0.0f);

		numberSampleCollected = new AtomicInteger(0);
		minutes = new AtomicInteger(0);
		seconds = new AtomicInteger(0);
		totalTime = new AtomicLong(0);
		currentTimeClock = new AtomicLong(0);
		startTime = new AtomicLong(0);

		//	linked list for all the value recorded
		xAxisListValue = new LinkedList<Float>();
		yAxisListValue = new LinkedList<Float>();
		zAxisListValue = new LinkedList<Float>();

		// vector that will contain the values of all the axis
		allValuesVector = new Vector<Iterator<Float>>(3); 
	}

	/**
	 * This class create the connection with the sensor and register the listener.
	 */
	private void initSensor(){
		mySensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
		mySensor = mySensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
		mySensorListener = new SensorListener();

		mySensorManager.registerListener(
				mySensorListener, mySensor, 
				SensorManager.SENSOR_DELAY_GAME);
	}

	/**
	 * Here i create and initialize my Details object, used to create the XML
	 */
	private void initDetails(){
		Calendar tempCalendar = Calendar.getInstance();
		myDetails = new Details();

		// formatting string data
		String tempString = "";
		if(tempCalendar.get(Calendar.DAY_OF_MONTH) < 10)
			tempString +="0"+tempCalendar.get(Calendar.DAY_OF_MONTH);
		else
			tempString +=""+tempCalendar.get(Calendar.DAY_OF_MONTH);

		if(tempCalendar.get(Calendar.MONTH) < 10)
			tempString +="/0"+tempCalendar.get(Calendar.MONTH);
		else
			tempString +="/"+tempCalendar.get(Calendar.MONTH);

		tempString +="/"+tempCalendar.get(Calendar.YEAR);
		myDetails.date = tempString;
		// formatting string ora
		tempString = "";
		if(tempCalendar.get(Calendar.HOUR_OF_DAY) < 10)
			tempString +="0"+tempCalendar.get(Calendar.HOUR_OF_DAY);
		else
			tempString +=""+tempCalendar.get(Calendar.HOUR_OF_DAY);

		if(tempCalendar.get(Calendar.MINUTE) < 10)
			tempString +=":0"+tempCalendar.get(Calendar.MINUTE);
		else
			tempString +=":"+tempCalendar.get(Calendar.MINUTE);

		if(tempCalendar.get(Calendar.SECOND) < 10)
			tempString +=":0"+tempCalendar.get(Calendar.SECOND);
		else
			tempString +=":"+tempCalendar.get(Calendar.SECOND);
		myDetails.time = tempString;

		tempString = "";
		// setting minutes
		int minutiSecondiTemp = (int)((totalTime.get()/1000)/60);
		if(minutiSecondiTemp < 10)
			tempString += "0" + minutiSecondiTemp;
		else
			tempString += "" + minutiSecondiTemp;

		tempString += ":";
		// settings seconds
		minutiSecondiTemp = (int)((totalTime.get()/1000)%60);
		if(minutiSecondiTemp < 10)
			tempString += "0" + minutiSecondiTemp;
		else
			tempString += "" + minutiSecondiTemp;

		// put the info inside the Details object
		myDetails.duration = tempString;
		myDetails.sample_rate = samplingRate+"";
		myDetails.x = xAxisEnabled;
		myDetails.y = yAxisEnabled;
		myDetails.z = zAxisEnabled;
	}

	@Override
	public void onCreate() {
		super.onCreate();
		initSystemForRecording(); // creating base variables
		showNotification(); // showing notification
		initSensor(); // preparing sensor
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		mySensorManager.unregisterListener(mySensorListener); 
		notificationManager.cancel(R.string.service_started); // Cancel the persistent notification.
		myHandler.removeCallbacks(worker); // remove the worker runnable if not already removed
	}

	/**
	 * This method will print the info in the top bar of the phone
	 */
	private void showNotification() {
		notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
		CharSequence text = getText(R.string.service_started);
		// Set the icon, scrolling text and timestamp
		Notification notification = new Notification(R.drawable.icon, text, System.currentTimeMillis());
		// The PendingIntent to launch our activity if the user selects this notification
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, Recording.class), 0);
		// Set the info for the views that show in the notification panel.
		notification.setLatestEventInfo(this, getText(R.string.service_label), text, contentIntent);
		// Send the notification.
		// We use a layout id because it is a unique number.  We use it later to cancel.
		notificationManager.notify(R.string.service_started, notification);
	}

	/**
	 * Message sent to the activity to stop the current Service.
	 * With it i send the duration of the current session, due to the fact that the user can stop the 
	 * recording. The string will be placed into RecordingEnded -> elapsedTimeValueTextView TextView
	 */
	private void stopMe(String duration){
		try {
			Bundle b = new Bundle();
			b.putString("duration", ""+duration);
			Message msg = Message.obtain(null, MSG_STOP_SERVICE);
			msg.setData(b);
			mClients.get(mClients.size()-1).send(msg);
		} catch (RemoteException e) {
			// The client is dead. Remove it from the list
			mClients.clear();
		}
	}

	/**
	 * This runnable will record every value with the specific sample time, after that it will save
	 * everything in a XML file.
	 */
	public Runnable worker = new Runnable() {

		public void run() {
			currentTimeClock.set(System.currentTimeMillis() - startTime.get());
			seconds.set( (int) ((currentTimeClock.get() / 1000) % 60)); // for displaying seconds
			minutes.set( (int) ((currentTimeClock.get() / 1000) / 60)); // for displaying minutes

			numberSampleCollected.incrementAndGet();
			if(mClients.size() != 0){
				try {
					// sending the number of sample collected
					// only at the last client registered, because on every rotation we will lose the connection
					// with the previous activity.
					mClients.get(mClients.size()-1).send(Message.obtain(null, MSG_SENDING_SAMPLE_COLLECTED, numberSampleCollected.get(), 0));

					// sending the elapsed time
					Bundle b = new Bundle();
					b.putString("elapsedTime",minutes+" m "+seconds+" s");
					Message msg = Message.obtain(null, MSG_SENDING_ELAPSED_TIME);
					msg.setData(b);
					mClients.get(mClients.size()-1).send(msg);

				} catch (RemoteException e) {
					// The client is dead. Remove it from the list
					mClients.clear();
				}
			}
			
			// adding sample (where enabled)
			if(xAxisEnabled)
				xAxisListValue.add(xAxisTempValue.get());
			if(yAxisEnabled)
				yAxisListValue.add(yAxisTempValue.get());
			if(zAxisEnabled)
				zAxisListValue.add(zAxisTempValue.get());

			if(currentTimeClock.get() < totalTime.get()){
				//	still time to work, set the runnable for a new running
				myHandler.postDelayed(this,samplingRate - 20);
			}
			else{
				// time's up or the user stopped the recording process. Time to save.
				myHandler.removeCallbacks(worker);

				if(xAxisEnabled)
					allValuesVector.add(xAxisListValue.iterator());
				if(yAxisEnabled)
					allValuesVector.add(yAxisListValue.iterator());
				if(zAxisEnabled)
					allValuesVector.add(zAxisListValue.iterator());

				try {// save current session on disk
					DataWorker.writeSessionOnDisk(myDetails, allValuesVector, getBaseContext());
				} catch (FileNotFoundException e) {

				}
				finally{
					if(mClients.size() == 0){
						// if no one is listening, stop the service and bring in front the ListSessionsActivity
						Intent dialogIntent = new Intent(getBaseContext(), ListSessionsActivity.class);
						dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
						getApplication().startActivity(dialogIntent);
						stopSelf();
					}
					else // if someone is listening, bring in front the RecordingEnded activity
						stopMe(myDetails.duration);
				}
			}
		}
	};

	/**
	 * This class only retrieve the value from the sensor and put it into the specific variables.
	 * So the handler can use them.
	 * @author Luca Lazzarini
	 *
	 */
	private class SensorListener implements SensorEventListener{
		private float maxGValue = 2.0f; // maximum G Force recorded
		private float maxValueRecorded = SensorManager.GRAVITY_EARTH * maxGValue;

		@Override
		public void onAccuracyChanged(Sensor arg0, int arg1) { // nothing to do
		}

		@Override
		public void onSensorChanged(SensorEvent event) {
			// normalizing input. Not above the maxValue
			if(event.values[0] > maxValueRecorded)
				event.values[0] = maxValueRecorded;
			if(event.values[0] < (-maxValueRecorded))
				event.values[0] = -maxValueRecorded;
			if(event.values[1] > maxValueRecorded)
				event.values[1] = maxValueRecorded;
			if(event.values[1] < (-maxValueRecorded))
				event.values[1] = -maxValueRecorded;
			if(event.values[2] > maxValueRecorded)
				event.values[2] = maxValueRecorded;
			if(event.values[2] < (-maxValueRecorded))
				event.values[2] = -maxValueRecorded;

			// Simply put the actual value into the variables
			xAxisTempValue.set(event.values[0]);
			yAxisTempValue.set(event.values[1]);
			zAxisTempValue.set(event.values[2]);
		}	
	}
}
